Skip to content

feat: add return policy extension (dev.ucp.shopping.policy.return) - #634

Open
amithanda wants to merge 1 commit into
mainfrom
feat/policy-return-extension
Open

feat: add return policy extension (dev.ucp.shopping.policy.return)#634
amithanda wants to merge 1 commit into
mainfrom
feat/policy-return-extension

Conversation

@amithanda

Copy link
Copy Markdown
Contributor

Defines the body of the dev.ucp.shopping.policy.return policy type on the core
policies[] primitive (#572), adding pre-purchase, machine-readable return terms
across catalog, cart, checkout, and order. The primitive's container, applies_to
targeting, same-type precedence, and messages[] disclosure are unchanged; this
extension only structures the type-specific body.

Context

What this adds

  • source/schemas/shopping/policy_return.json - the extension. Composes onto
    catalog.search, catalog.lookup, cart, checkout, and order.
  • docs/specification/return.md - the spec.
  • mkdocs.yml - nav entry and llms.txt section.

Five type-specific fields, all optional (the base policy already guarantees type
and description):

  • final_sale - non-returnable flag.
  • window { days, anchor } - the return window as a policy statement.
  • supported_resolutions[] - what the buyer gets back (money, store credit, exchange).
  • methods[] - return channels, each with its logistics fee.
  • restocking_fee - a refund deduction independent of channel.

Design decisions

  1. Extension, not core; type body gated by if type == ... then <body>. Because
    the base policy sets additionalProperties: true, a platform that has not
    negotiated the type still renders the policy from type + description, and
    unknown policy types are never rejected. The container is untouched.
  2. Targeting via the primitive's applies_to, not item -> policy ids (the feat: add policies[] to cart, checkout, catalog, and order #572
    decision). A response-wide default is one entry with no applies_to; overrides
    target specific items; precedence is inherited from the primitive.
  3. Five fields, by a deliberate inclusion bar. A structured field ships only if
    it is needed to compute or compare on (not merely display) and reduces to a
    shape that is closed-and-final (boolean, integer) or open-by-design (string
    vocab), so future additions stay non-breaking. Everything else stays in
    description/url until its shape settles.
  4. Two costs modeled separately: per-channel logistics methods[].fee vs. a
    policy-level restocking_fee. This resolves the limitation raised in feat: add Return Extension to UCP specification #257
    (§1.4): "free returns by mail with a restocking fee" is inexpressible when a
    single per-method fee must be free xor fixed_fee.
  5. Explicit window.anchor, no default. Anchors genuinely vary, so window
    requires both days and anchor.
  6. Open-string vocabularies throughout (anchor, supported_resolutions,
    method.type, fee.type), never enum, so new channels and outcomes are
    non-breaking additions.

Potential future additions

Fields we may add to this type later, once their shape settles. Each would be an
optional, non-breaking addition; until then the information is carried in
description/url:

  • Resolved window.deadline - the absolute cutoff, computed once the anchor
    event (e.g., delivery) fires; added with the Order-side resolution work.
  • Item-condition eligibility - which conditions are returnable (new / used /
    damaged / refurbished). The strongest near-term candidate.
  • Reason-differentiated fees - distinct fee/label for buyer remorse vs. item
    defect.
  • Seasonal / holiday window overrides - dated changes to the return window.
  • Richer fee breakdown - itemizing a logistics fee into components
    (e.g., shipping + handling).
  • Explicit unlimited-window signal - to distinguish "explicitly unlimited"
    from "not stated structurally."

Non-goals

Out of scope for this type; they belong to other extensions:

  • Post-purchase resolution - RMA issuance, label generation, refund/exchange
    execution, and reason/condition capture at return time. A separate future
    extension; this type must not grow those fields.
  • Tiered, anchor-relative cancellation - stepped partial-refund schedules for
    bookings/events. A sibling dev.ucp.shopping.policy.cancellation type over the
    same primitive.

Category

  • Capability: New schemas (Discovery, Cart, etc.) or extensions. (Requires Maintainer approval)
  • Documentation: Updates to README, or documentations regarding schema or capabilities. (Requires Maintainer approval)

Related Issues

Builds on #572 (policies primitive). Supersedes the standalone approach in #257.

Checklist

  • I have followed the Contributing Guide (Conventional Commits; additive, no !).
  • I have updated the documentation (return.md, mkdocs nav, llms.txt).
  • My changes pass all local linting and formatting checks.
  • I have added tests that prove the feature works (schema-validated ucp:example blocks in return.md, checked by the corpus example validator).
  • New and existing unit tests pass locally with my changes.
  • (For Core/Capability) I have included/updated the relevant JSON schemas.

Defines the dev.ucp.shopping.policy.return policy type on the core
policies[] primitive (#572), adding pre-purchase, machine-readable return
terms across catalog, cart, checkout, and order:

- window { days, anchor } as a policy statement with an explicit anchor
- supported_resolutions (what the buyer gets back)
- methods[] with per-channel logistics fee
- policy-level restocking_fee (deduction independent of channel)
- final_sale flag for non-returnable items

The base policies[] container, applies_to targeting, precedence, and
messages[] disclosure are unchanged; the extension only structures the
type-specific body, gated by an if/then on type so unknown policy types
are never rejected. Incorporates PR #257 feedback (open-string
vocabularies, collapsed $defs, explicit anchor, separated logistics vs
restocking cost).
@damaz91 damaz91 added the status:needs-triage Signal that the PR is ready for human triage label Jul 24, 2026
@amithanda amithanda added the TC review Ready for TC review label Jul 25, 2026
@damaz91 damaz91 added status:under-review gov:needs-tc-review and removed status:needs-triage Signal that the PR is ready for human triage labels Jul 27, 2026
@igrigorik igrigorik added this to the Working Draft milestone Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants